Micron Document
🎖️GitЯра🎖️

Node / meshtastic / Meshtastic-Android / files / androidApp / src / test / kotlin / org / meshtastic / app / firmware / SoftDeviceQuirkCoverageTest.kt

Displaying Raw • Download

androidApp/src/test/kotlin/org/meshtastic/app/firmware/SoftDeviceQuirkCoverageTest.kt bd2863243bab6eb213401d949839a2bc74dde7e2 (bd286324) Text, 7.09 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.app.firmware

Tff7b72import T7ee787kotlinx.serialization.json.Json
Tff7b72import T7ee787org.meshtastic.core.model.BootloaderOtaQuirksResponse
Tff7b72import T7ee787org.meshtastic.core.model.NetworkDeviceHardware
Tff7b72import T7ee787org.meshtastic.core.model.SoftDeviceVariant
Tff7b72import T7ee787java.io.File
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertNotNull
Tff7b72import T7ee787kotlin.test.assertTrue
Tff7b72import T7ee787kotlin.test.fail

T8b949e/**
* Guards the SoftDevice map in `device_bootloader_ota_quirks.json` against the hardware catalog.
*
* This test is the reason the map can safely be fail-closed. An unmapped or mistyped row does not break anything at
* runtime — it silently makes factory erase unavailable for that device — so nothing else would ever notice. CI has to
* be what notices.
*
* It deliberately does **not** assert which variant each board uses, only that every board has one and that the values
* are recognised. The variant itself is verified at runtime against what the device reports on its UF2 drive, and a
* disagreement refuses rather than guessing (see `resolveNrfEraseImage`).
*/
Tff7b72class T56d364SoftDeviceQuirkCoverageTest Tb4b4b4{

T8b949e/**
* Models with no firmware variant on `meshtastic/firmware` master, so no ldscript exists to derive a SoftDevice
* from. Present in the asset with no `softDevice` value, which makes the refusal deliberate and greppable rather
* than an oversight. Remove an entry here once its variant lands upstream.
*/
Tff7b72private Tff7b72val Te6edf3knownUnmapped Tff7b72= Te6edf3setOfTb4b4b4(T79c0ff1T79c0ff3T79c0ff0Tb4b4b4) T8b949e// THINKNODE_M8

Tff7b72private Tff7b72val Te6edf3json Tff7b72= Te6edf3Json Tb4b4b4{ Te6edf3ignoreUnknownKeys Tff7b72= Tff7b72true Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`every nrf52840 model in the hardware catalog has a softdevice mapping`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3nrfModels Tff7b72= Te6edf3nrfHardwareTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3hwModel Tb4b4b4}Tb4b4b4.Te6edf3toSetTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3mapped Tff7b72= Te6edf3quirksTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3softDeviceVariantsTb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3softDevice Tff7b72!Tff7b72= Tff7b72null Tb4b4b4}Tb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3hwModel Tb4b4b4}Tb4b4b4.Te6edf3toSetTb4b4b4(Tb4b4b4)

Tff7b72val Te6edf3missing Tff7b72= Te6edf3nrfModels Tff7b72- Te6edf3mapped Tff7b72- Te6edf3knownUnmapped
Te6edf3assertTrueTb4b4b4(
Te6edf3missingTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffnRF52840 models with no SoftDevice mapping: Tffd700$Te6edf3missingTa5d6ff. Factory erase is silently unavailable for these. Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffDerive each from meshtastic/firmware boards/<board>.json build.arduino.ldscript Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ff(nrf52840_s140_v6.ld = 6.1.1, _v7.ld = 7.3.0), or add the hwModel to knownUnmapped with a reason.Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`known-unmapped models are present but deliberately carry no softdevice`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3entries Tff7b72= Te6edf3quirksTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3softDeviceVariantsTb4b4b4.Te6edf3associateBy Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3hwModel Tb4b4b4}
Te6edf3knownUnmappedTb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3hwModel Tff7b72-Tff7b72>
Tff7b72val Te6edf3entry Tff7b72= Te6edf3entriesTff7b72[Te6edf3hwModelTff7b72]
Te6edf3assertNotNullTb4b4b4(
Te6edf3entryTb4b4b4,
Ta5d6ff"Ta5d6ffhwModel Tffd700$Te6edf3hwModelTa5d6ff is listed as known-unmapped but has no row at all — add the row so Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffthe refusal is explicit, or drop it from knownUnmappedTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Tff7b72nullTb4b4b4,
Te6edf3entryTb4b4b4.Te6edf3softDeviceTb4b4b4,
Ta5d6ff"Ta5d6ffhwModel Tffd700$Te6edf3hwModelTa5d6ff is listed as known-unmapped but now carries a SoftDevice — remove it from Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffknownUnmapped insteadTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`every softdevice value is one the app ships an erase image for`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3quirksTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3softDeviceVariantsTb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3entry Tff7b72-Tff7b72>
Tff7b72val Te6edf3wire Tff7b72= Te6edf3entryTb4b4b4.Te6edf3softDevice Tff7b72?: Tff7b72returnTf0883e@forEach
Te6edf3assertNotNullTb4b4b4(
Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3fromWireTb4b4b4(Te6edf3wireTb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffhwModel Tffd700${Te6edf3entryTb4b4b4.Te6edf3hwModelTffd700}Ta5d6ff (Tffd700${Te6edf3entryTb4b4b4.Te6edf3hwModelSlugTffd700}Ta5d6ff) declares SoftDevice 'Tffd700$Te6edf3wireTa5d6ff', which maps to no Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6fferase image. A typo here disables erase for that device silently.Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`every mapped target exists in the hardware catalog`Tb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Resolution requires the device-reported target to appear in its row, so a stale target name is a silent
T8b949e// refusal for that build.
Tff7b72val Te6edf3catalogTargets Tff7b72= Te6edf3nrfHardwareTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3platformioTarget Tb4b4b4}Tb4b4b4.Te6edf3toSetTb4b4b4(Tb4b4b4)
Te6edf3quirksTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3softDeviceVariantsTb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3entry Tff7b72-Tff7b72>
Te6edf3entryTb4b4b4.Te6edf3platformioTargetsTb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3target Tff7b72-Tff7b72>
Te6edf3assertTrueTb4b4b4(
Te6edf3target Tff7b72in Te6edf3catalogTargetsTb4b4b4,
Ta5d6ff"Ta5d6ffhwModel Tffd700${Te6edf3entryTb4b4b4.Te6edf3hwModelTffd700}Ta5d6ff lists platformioTarget 'Tffd700$Te6edf3targetTa5d6ff', which is not in the hardware Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffcatalog — erase would be refused for any device reporting itTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`every nrf52840 catalog target is covered by its model's row`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3entries Tff7b72= Te6edf3quirksTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3softDeviceVariantsTb4b4b4.Te6edf3associateBy Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3hwModel Tb4b4b4}
Te6edf3nrfHardwareTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3hw Tff7b72-Tff7b72>
Tff7b72val Te6edf3entry Tff7b72= Te6edf3entriesTff7b72[Te6edf3hwTb4b4b4.Te6edf3hwModelTff7b72] Tff7b72?: Tff7b72returnTf0883e@forEach
Te6edf3assertTrueTb4b4b4(
Te6edf3hwTb4b4b4.Te6edf3platformioTargetsTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3any Tb4b4b4{ Tffa657it Tff7b72in Te6edf3entryTb4b4b4.Te6edf3platformioTargets Tb4b4b4}Tb4b4b4,
Ta5d6ff"Ta5d6ffhwModel Tffd700${Te6edf3hwTb4b4b4.Te6edf3hwModelTffd700}Ta5d6ff (Tffd700${Te6edf3hwTb4b4b4.Te6edf3hwModelSlugTffd700}Ta5d6ff) builds target 'Tffd700${Te6edf3hwTb4b4b4.Te6edf3platformioTargetTffd700}Ta5d6ff' but its row lists Ta5d6ff" Tff7b72+
Ta5d6ff"Tffd700${Te6edf3entryTb4b4b4.Te6edf3platformioTargetsTffd700}Ta5d6ff — a device reporting that target would be refusedTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`mesh tracker x1 is mapped, since the web flasher omits it`Tb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// hwModel 128 is nrf52840_s140_v7.ld (7.3.0) but is absent from the web flasher's sd7.3 allowlist, so that tool
T8b949e// serves it the 6.1.1 image. Pinned here so the app does not inherit the same gap.
Tff7b72val Te6edf3entry Tff7b72= Te6edf3quirksTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3softDeviceVariantsTb4b4b4.Te6edf3firstOrNull Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3hwModel Tff7b72=Tff7b72= Te6edf3MESH_TRACKER_X1_HW_MODEL Tb4b4b4}
Te6edf3assertNotNullTb4b4b4(Te6edf3entryTb4b4b4, Ta5d6ff"Ta5d6ffMESH_TRACKER_X1 (128) must be mappedTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_7_3_0Tb4b4b4, Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3fromWireTb4b4b4(Te6edf3entryTb4b4b4.Te6edf3softDeviceTb4b4b4)Tb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Te6edf3NetworkDeviceHardwareTb4b4b4.Td2a8ffplatformioTargetsTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Tffa657StringTff7b72> Tff7b72= Te6edf3listOfTb4b4b4(Te6edf3platformioTargetTb4b4b4)

Tff7b72private Tff7b72fun Td2a8ffnrfHardwareTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3NetworkDeviceHardwareTff7b72> Tff7b72=
Te6edf3jsonTb4b4b4.Te6edf3decodeFromStringTff7b72<Te6edf3ListTff7b72<Te6edf3NetworkDeviceHardwareTff7b72>Tff7b72>Tb4b4b4(Te6edf3assetTb4b4b4(Ta5d6ff"Ta5d6ffdevice_hardware.jsonTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3readTextTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3filter Tb4b4b4{
Tffa657itTb4b4b4.Te6edf3architecture Tff7b72=Tff7b72= Te6edf3NRF52840_ARCHITECTURE
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffquirksTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3BootloaderOtaQuirksResponse Tff7b72=
Te6edf3jsonTb4b4b4.Te6edf3decodeFromStringTb4b4b4(Te6edf3assetTb4b4b4(Ta5d6ff"Ta5d6ffdevice_bootloader_ota_quirks.jsonTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3readTextTb4b4b4(Tb4b4b4)Tb4b4b4)

Tff7b72private Tff7b72fun Td2a8ffassetTb4b4b4(Te6edf3nameTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Te6edf3File Tff7b72=
Te6edf3listOfTb4b4b4(Ta5d6ff"Ta5d6ffsrc/main/assets/Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffandroidApp/src/main/assets/Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3mapTb4b4b4(Tff7b72::Te6edf3FileTb4b4b4)Tb4b4b4.Te6edf3firstOrNullTb4b4b4(Te6edf3FileTff7b72::Te6edf3existsTb4b4b4)
Tff7b72?: Te6edf3failTb4b4b4(Ta5d6ff"Ta5d6ffCould not locate Tffd700$Te6edf3nameTa5d6ff from working directory Tffd700${Te6edf3FileTb4b4b4(Ta5d6ff"Tb4b4b4.Ta5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3absolutePathTffd700}Ta5d6ff"Tb4b4b4)

Tff7b72private Tff7b72companion Tff7b72object Tb4b4b4{
Tff7b72const Tff7b72val Te6edf3NRF52840_ARCHITECTURE Tff7b72= Ta5d6ff"Ta5d6ffnrf52840Ta5d6ff"
Tff7b72const Tff7b72val Te6edf3MESH_TRACKER_X1_HW_MODEL Tff7b72= T79c0ff1T79c0ff2T79c0ff8
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.06s